Add support for HTTP/2 - #13039
Conversation
This implementation is backwards compatible, functional, but still incomplete.
for more information, see https://pre-commit.ci
| self._handler: Optional[asyncio.Protocol] = None | ||
|
|
||
| # ---- Transport callbacks forwarded to the real handler ---- | ||
| def connection_made(self, transport: asyncio.BaseTransport) -> None: |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## master #13039 +/- ##
==========================================
- Coverage 98.98% 98.94% -0.04%
==========================================
Files 132 139 +7
Lines 49073 50403 +1330
Branches 2553 2647 +94
==========================================
+ Hits 48576 49873 +1297
- Misses 373 392 +19
- Partials 124 138 +14
Flags with carried forward coverage won't be shown. Click here to find out more. ☔ View full report in Codecov by Harness. |
Merging this PR will degrade performance by 9.65%
|
| Benchmark | BASE |
HEAD |
Efficiency | |
|---|---|---|---|---|
| ❌ | test_one_thousand_round_trip_websocket_binary_messages[tcp-small] |
46.8 ms | 54.4 ms | -14.11% |
| ❌ | test_one_thousand_round_trip_websocket_text_messages |
48.1 ms | 55.3 ms | -12.95% |
| ❌ | test_one_hundred_simple_get_requests_multiple_methods_route |
137 ms | 149.9 ms | -8.6% |
| ❌ | test_one_hundred_simple_get_requests_alternating_clients |
140.5 ms | 153.5 ms | -8.5% |
| ❌ | test_one_hundred_simple_get_requests[tcp] |
137.9 ms | 150.6 ms | -8.43% |
| ❌ | test_one_hundred_get_requests_with_1024_content_length_payload |
148.3 ms | 161.6 ms | -8.2% |
| ❌ | test_one_hundred_get_requests_with_1024_chunked_payload[tcp] |
150.2 ms | 163.5 ms | -8.13% |
| ❌ | test_ten_web_middlewares |
146.7 ms | 159.5 ms | -8.02% |
Tip
Investigate this regression by commenting @codspeedbot fix this regression on this PR, or directly use the CodSpeed MCP with your agent.
Comparing Moist-Cat:master (742899f) with master (d5d068c)2
Footnotes
-
83 benchmarks were skipped, so the baseline results were used instead. If they were deleted from the codebase, click here and archive them to remove them from the performance reports. ↩
-
No successful run was found on
master(c0ef574) during the generation of this report, so d5d068c was used instead as the comparison base. There might be some changes unrelated to this pull request in this report. ↩
|
|
||
| try: | ||
| import sphinxcontrib.spelling # noqa | ||
| import sphinxcontrib.spelling |
It was necessary to add a semaphore to ensure the requests connect sequentially to the hosts and reuse connections when necessary. HTTP/2 uses a single connection per host.
for more information, see https://pre-commit.ci
|
I ran tests against remote servers (httpbin.org) to verify HTTP/2 indeed reduces latency. HTTP/2 Performance Test ResultsSystem Specs:
Test Configuration:
Batch Mean Latency (seconds)
Individual Request Latency Distribution
Statistical Analysis
A simple bar chart with the means (results vary because they are from a second test): We lose efficiency in CPU bound tasks (see #13039 (comment)) but I/O bound tasks are significantly faster. This is specially true for batch requests that require multiple TCP connections to the same host. |
|
I would like to know if the trade-offs (I/O vs CPU) are acceptable before writing the docs. |
HTTP/1.1 regression not inherent to h2. Caused by global |
Bigger blocker than the CPU/IO trade-off. h2 path returns |
|
|
|
Either inheriting from or using Regarding the To deal with |
At a glance, it looks like there's still a lot of shared code. I'd suggest a refactor that produces a generic ClientResponse and then have both versions subclass it. So we end up with ClientResponseHttp1 and ClientResponseHttp2 or similar. It would really help us to review if that refactor was in a separate PR. Also consider that we can make modest breaking changes in v4, if that's required for a clean solution. I don't have capacity to review the PR in full yet, but I'll come back round to it before the 3.15 release. Might be worth a couple of rounds with the bot before then. Thanks for looking into this complex feature. |
|
Also, we generally don't use envvars, I'd probably add this as a ClientSession parameter instead. |
| import json | ||
| import os | ||
| import sys | ||
| import time |
I will open another PR with the refactored code.
I'm aware but I would rather not change more lines than necessary before the PR is ready to merge to avoid conflicts. Switching to a session-level parameter can be done later. |
|
|
||
| # Send initial SETTINGS (our preferences) | ||
| settings_payload = struct.pack( | ||
| "!H I", Setting.ENABLE_PUSH, 0 # disable server push |
There was a problem hiding this comment.
But push is enabled in settings.py:DEFAULT_SETTINGS
There was a problem hiding this comment.
Correct — real inconsistency. initiate_connection sends ENABLE_PUSH=0 on wire. local_settings still holds 1. Two sources of truth diverge.
Worse consequence downstream: _dispatch_frame only logs on PUSH_PROMISE. RFC 9113 §6.6 requires connection error when push disabled. Silent drop also skips HPACK decode, so peer's dynamic table diverges from ours. Every later header block corrupts. Same argument applies to dropped CONTINUATION frames.
Suggest separate local defaults with ENABLE_PUSH: 0, plus _protocol_error() on both frame types.
|
In short, what impedes the support for |
Strong argument for session parameter beyond convention. So Also current gate leaks: |
Coupling point noted, and @Dreamsorcerer's shared-base-class split addresses it — On "public interface same": not yet. Concrete gaps found: Full list in the deferred finding — checklist for the refactor PR. |
Agreed on scope split. One correction on semaphore though. The unconditional So flag-gating it isn't just perf tuning; it removes a new cross-host stall. Worth doing in this PR. |
Nothing structural blocks h2c. Missing pieces: (1) prior-knowledge entry point — skip ALPN, call Suggest waiting for the |
PR Review — Add support for HTTP/2Substantial, well-tested HTTP/2 core — but the integration layer still regresses the default HTTP/1.1 path and the frame parser has several inputs that crash the connection. What's genuinely strong here:
Blocking on the default path (affects every aiohttp user, flag or no flag):
Blocking on the h2 path:
The ✅ Resolved since last review (6)Previously-flagged issues verified fixed
🔴 Blocking
1. Global Semaphore(1) serializes every connect — including HTTP/1.1 — and head-of-line-blocks unrelated hosts
|
I suppose you could add it without making major architectural changes. In principle, only two changes are needed:
For example, TL;DR: Add the headers to the request and switch protocols inside the Semaphore. Subsequent requests will reuse the Considerations:
I would rather not implement this because, regardless of what we do with the semaphore, we have to wait until we get the first response from the server to know which protocol we should use. Notice that this doesn't happen if we negotiate the protocol with ALPN. |
|
I just noticed that I'm the one who said that support for |
|
h2c is widely unsupported anyway (no major browser supports it), so we don't need to focus on that. If it's easy to add later, we can do so, but let's try not to expand the scope of this current work. |
|
|

What do these changes do?
Add HTTP/2 client support.
Why
Faster I/O bound operations (e.g., many requests to the same host) via multiplexing (handling several streams/requests inside a single connection).
How
AIOHTTP_ENABLE_EXPERIMENTAL_PROTOCOLS=1to allowh2negotiation via ALPN during the TLS handshake.ResponseHandlerwas substituted by a wrapper that conditionally switches protocols depending on the negotiated protocol.Semaphoreto avoid race conditions.This means opening many HTTP/1.1 connections in parallel is now slower because it's done sequentially. That said, to know if connections can be pooled or not it's only necessary to wait until the first connection is done. Once it's known whether the host supports HTTP/2 or not, the rest of the requests can be done in parallel so it's possible to mitigate this performance hit substantially.
Backward compatibility
Opt-in via
AIOHTTP_ENABLE_EXPERIMENTAL_PROTOCOLS=1.Testing
%95 coverage, benchmarks (%50 latency reduction for 99 requests, see below), and integration tests against real servers (~100).
Dependencies
hpack
Is it a substantial burden for the maintainers to support this?
Yes.
Related issue number
refs #5999
The implementation is self-contained, the changes to the current codebase are minimal and backwards compatible. That said, I make use of some black magic with
__getattr__to be able to conditionally switch protocols.Missing features (to the date):